// SPDX-License-Identifier: Apache-2.0

package chisel3

import chisel3.experimental.{BaseModule, Param}
import chisel3.internal.BaseBlackBox
import chisel3.internal.Builder.pushCommand
import chisel3.internal.firrtl._
import chisel3.internal.throwException
import chisel3.experimental.{SourceInfo, UnlocatableSourceInfo}
import scala.annotation.nowarn

package internal {

  private[chisel3] abstract class BaseBlackBox extends BaseModule {
    private[chisel3] def _isImportedDefinition: Boolean = false
  }
}

package experimental {

  sealed abstract class Param
  case class IntParam(value: BigInt) extends Param
  case class DoubleParam(value: Double) extends Param
  case class StringParam(value: String) extends Param

  case class RawParam(value: String) extends Param

  @nowarn("msg=class Port") // delete when Port becomes private
  abstract class ExtModule(val params: Map[String, Param] = Map.empty[String, Param]) extends BaseBlackBox {
    private[chisel3] override def generateComponent(): Option[Component] = {
      require(!_closed, "Can't generate module more than once")
      _closed = true

      namePorts()

      val firrtlPorts = getModulePorts.map {
        case port => Port(port, port.specifiedDirection, UnlocatableSourceInfo)
      }
      val component = DefBlackBox(this, name, firrtlPorts, SpecifiedDirection.Unspecified, params)
      _component = Some(component)
      _component
    }

    private[chisel3] def initializeInParent(): Unit = {
      implicit val sourceInfo = UnlocatableSourceInfo
    }
  }
}

@nowarn("msg=class Port") // delete when Port becomes private
abstract class BlackBox(
  val params: Map[String, Param] = Map.empty[String, Param])
    extends BaseBlackBox {

  private[chisel3] lazy val _io: Option[Record] =
    this
      .findPort("io")
      .collect { case r: Record => r } // Must be a Record

  protected def _compatIoPortBound() = _io.exists(portsContains(_))

  private[chisel3] override def generateComponent(): Option[Component] = {
    if (!_io.exists(portsContains)) {
      throwException(s"BlackBox '$this' must have a port named 'io' of type Record wrapped in IO(...)!")
    }

    require(portsSize == 1, "BlackBox must only have one IO, called `io`")

    require(!_closed, "Can't generate module more than once")
    _closed = true

    val io = _io.get

    val namedPorts = io.elements.toSeq.reverse // ListMaps are stored in reverse order

    for ((name, port) <- namedPorts) {
      this.findPort("io").get.setRef(ModuleIO(internal.ViewParent, ""), force = true)
      port.setRef(ModuleIO(this, _namespace.name(name)), force = true)
    }

    val firrtlPorts = namedPorts.map { namedPort =>
      Port(namedPort._2, namedPort._2.specifiedDirection, UnlocatableSourceInfo)
    }
    val component = DefBlackBox(this, name, firrtlPorts, io.specifiedDirection, params)
    _component = Some(component)
    _component
  }

  private[chisel3] def initializeInParent(): Unit = {}
}
